package alessia.ldap.authenticator;

import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import java.util.Properties;
import java.io.InputStream;
import java.io.IOException;

/**
 * *************************************************************************************************
 *
 *
 * This file is part of Alessia Authenticator - a Java interface to authenticate
 * your application via LDAP. For details, please see
 * https://code.google.com/p/alessia-authenticator/
 *
 * Copyright (c) 2014 Giampaolo Franco
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 * Getting Source ==============
 *
 * Source for this application is maintained at code.google.com, a repository
 * for free software projects.
 *
 * For details, please see https://code.google.com/p/alessia-authenticator/
 *
 * @author Giampaolo Franco <a
 * href="https://code.google.com/p/alessia-authenticator/">alessia-authenticator</a>
 * @created June 05, 2014
 */
public class AlessiaAuthenticator {

    private Logger logger;
    private DirContext userDirContext;
    private DirContext initialDirContext;
    private Properties properties;
    private InputStream input;
    private String ldapServerURL;
    private String ldapSecurityAuthentication;
    private String ldapBaseAddress;
    private String ldapServiceDName;
    private String ldapServiceDNamePasswd;
    private String ldapSearchKey;
    private String ldapSearchFilter;
    private String ldapDisplayName;
    private static AlessiaAuthenticator instance;
    private final String propertyFilePath;

    private AlessiaAuthenticator(String propertyFilePath) {
        this.propertyFilePath = propertyFilePath;
        // init log4j
        logger = Logger.getLogger(AlessiaAuthenticator.class);
        PropertyConfigurator.configure(AlessiaAuthenticator.class.getClassLoader().getResource("log4j.properties"));
        logger.debug("AlessiaAuthenticator() [OK] ");
        //
        init();
    }

    public static synchronized AlessiaAuthenticator getResource(String propertyFilePath) {
        if (instance == null) {
            instance = new AlessiaAuthenticator(propertyFilePath);
        }
        return instance;
    }

    private void init() {
        // default constructor
        logger.debug("AlessiaAuthenticator() [OK] ");
        // init log4j
        logger = Logger.getLogger(AlessiaAuthenticator.class);
        PropertyConfigurator.configure(AlessiaAuthenticator.class.getClassLoader().getResource("log4j.properties"));
        // load application properties
        properties = new Properties();
        input = null;
        try {
            // load file stream 
            input = Thread.currentThread().getContextClassLoader().getResourceAsStream(propertyFilePath);
            //
            // load a properties file
            properties.load(input);
            // get the properties values
            ldapServerURL = properties.getProperty("ldap.server.url");
            ldapSecurityAuthentication = properties.getProperty("ldap.security.authentication");
            ldapBaseAddress = properties.getProperty("ldap.base.address");
            ldapServiceDName = properties.getProperty("ldap.service.dname");
            ldapServiceDNamePasswd = properties.getProperty("ldap.service.dname.passwd");
            ldapSearchKey = properties.getProperty("ldap.search.key");
            ldapSearchFilter = properties.getProperty("ldap.search.filter");
            ldapDisplayName = properties.getProperty("ldap.display.name");
            // print environment
            printEnvironment();
            //
        } catch (IOException e) {
            logger.error("init() ", e);
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    logger.error("init() ", e);
                }
            }
        }
    }

    private void printEnvironment() {
        logger.info("printEnvironment() ldapServerUrl: " + ldapServerURL);
        logger.info("printEnvironment() ldapSecurityAuthentication: " + ldapSecurityAuthentication);
        logger.info("printEnvironment() ldapBaseAddress: " + ldapBaseAddress);
        logger.info("printEnvironment() ldapServiceDName: " + ldapServiceDName);
        logger.info("printEnvironment() ldapServiceDNamePasswd: " + ldapServiceDNamePasswd);
        logger.info("printEnvironment() ldapSearchFilter: " + ldapSearchFilter);
        logger.info("printEnvironment() ldapDisplayName: " + ldapDisplayName);
    }

    private boolean createInitialDirContext() {
        logger.debug("createInitialDirContext() [start] ");
        setInitialDirContext(null);
        Boolean esito = false;
        try {
            Hashtable ldapEnv = new Hashtable(11);
            ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
            ldapEnv.put(Context.PROVIDER_URL, ldapServerURL);
            ldapEnv.put(Context.SECURITY_AUTHENTICATION, ldapSecurityAuthentication);
            ldapEnv.put(Context.SECURITY_PRINCIPAL, ldapServiceDName);
            ldapEnv.put(Context.SECURITY_CREDENTIALS, ldapServiceDNamePasswd);
            // Enable connection pooling
            //ldapEnv.put("com.sun.jndi.ldap.connect.pool", "true");
            setInitialDirContext(new InitialLdapContext(ldapEnv, null));
            esito = true;
            //
            logger.debug("createInitialDirContext() [OK] ");
        } catch (NamingException nex) {
            logger.error("createInitialDirContext() -> " + nex.toString());
        }
        return esito;
    }

    private boolean closeInitialDirContext() {
        try {
            getInitialDirContext().close();
            logger.debug("closeInitialDirContext() [OK] ");
            return true;
        } catch (NamingException nex) {
            logger.error("closeInitialDirContext() ", nex);
            return false;
        }
    }

    private boolean createUserDirContext(String dn, String password) {
        setUserDirContext(null);
        Boolean esito;
        try {
            Hashtable ldapEnv = new Hashtable(11);
            ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
            ldapEnv.put(Context.PROVIDER_URL, ldapServerURL);
            ldapEnv.put(Context.SECURITY_AUTHENTICATION, ldapSecurityAuthentication);
            ldapEnv.put(Context.SECURITY_PRINCIPAL, dn);
            ldapEnv.put(Context.SECURITY_CREDENTIALS, password);
            //Enable connection pooling
            //ldapEnv.put("com.sun.jndi.ldap.connect.pool", "true");
            setUserDirContext(new InitialLdapContext(ldapEnv, null));
            esito = true;
            logger.debug("createUserDirContext() [OK] ");
            //
        } catch (NamingException nex) {
            //logger.error("createUserDirContext", nex);
            esito = false;
        }
        return esito;
    }

    private boolean closeUserDirContext() {
        boolean isClosed = false;
        if (getUserDirContext() != null) {
            try {
                getUserDirContext().close();
                isClosed = true;
                logger.debug("closeInitialDirContext() [OK] ");
            } catch (NamingException nex) {
                logger.error("closeUserDirContext", nex);
                isClosed = false;
            }
        }
        return isClosed;

    }

    private String getDistinguishedName(String username) {
        String myDistinguishedName = "";
        try {
            SearchControls constraints = new SearchControls();
            constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
            String[] attrIDs = {ldapSearchKey};
            constraints.setReturningAttributes(attrIDs);
            //
            String base = ldapBaseAddress;
            String filter = ldapSearchFilter + "=" + username;
            //filter = "";
            NamingEnumeration answer = getInitialDirContext().search(base, filter, constraints);
            if (answer.hasMore()) {
                Attributes attrs = ((SearchResult) answer.next()).getAttributes();
                myDistinguishedName = attrs.get(ldapSearchKey).toString();
                logger.debug("getDistinguishedName() [OK] " + myDistinguishedName);
            } else {
                throw new Exception("Invalid User");
            }
        } catch (Exception ex) {
            logger.error("getDistinguishedName() String -> " + username, ex);
            myDistinguishedName = "";
        }
        return myDistinguishedName;
    }

    /**
     *
     * @param username
     * @return String getDisplayedName from LDAP by username (matricola 53xxxxx)
     */
    public String getDisplayedName(String username) {
        String myDisplayName = "";
        //
        createInitialDirContext();
        try {
            SearchControls constraints = new SearchControls();
            constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
            String[] attrIDs = {ldapDisplayName};
            constraints.setReturningAttributes(attrIDs);
            //
            String base = ldapBaseAddress;
            String filter = ldapSearchFilter + "=" + username;
            //filter = "";
            NamingEnumeration answer = getInitialDirContext().search(base, filter, constraints);
            if (answer.hasMore()) {
                Attributes attrs = ((SearchResult) answer.next()).getAttributes();
                myDisplayName = attrs.get(ldapDisplayName).toString();
            } else {
                throw new Exception("Invalid User");
            }
        } catch (Exception ex) {
            logger.debug("getDisplayedName() String -> ", ex);
            myDisplayName = "";
        }
        //close initial context
        closeInitialDirContext();
        //
        logger.debug("getDisplayedName() [OK] ");
        return myDisplayName.replaceAll(ldapDisplayName + ": ", "");
    }

    /**
     *
     * @param username
     * @param password
     * @return true if there is a valid authentication LDAP user/password
     */
    public boolean isValidUser(String username, String password) {
        boolean isValid = true;
        String myDN;
        try {
            //creo contesto initialDirContext loggandomi con utente "crash"
            createInitialDirContext();
            //ricerco user per reperire il DN della matricola inserita
            myDN = getDistinguishedName(username);
            //chiudo contesto iniziale
            closeInitialDirContext();
            //se ho trovato un DN allora ne controllo la password
            if (!myDN.equals("")) {
                //apro contesto per l'utente
                myDN = myDN.replace(ldapSearchKey + ": ", "");
                //
                isValid = createUserDirContext(myDN, password);
                //chiudo contesto utente
                closeUserDirContext();
                logger.debug("isValidUser() [OK] " + isValid);
            }
            //        
        } catch (Exception e) {
            isValid = false;
            logger.debug("isValidUser() ", e);
        }
        return isValid;
    }

    private DirContext getInitialDirContext() {
        return initialDirContext;
    }

    private void setInitialDirContext(DirContext initialDirContext) {
        this.initialDirContext = initialDirContext;
    }

    private DirContext getUserDirContext() {
        return userDirContext;
    }

    private void setUserDirContext(DirContext userDirContext) {
        this.userDirContext = userDirContext;
    }

}
